# coding: utf-8

'''
reference: https://github.com/jjkester/django-auditlog/blob/master/src/auditlog/middleware.py
'''

from __future__ import unicode_literals

import logging
import threading
import time

from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.db.models.signals import pre_save
from django.utils.functional import SimpleLazyObject
from functools import partial as curry
from rest_framework.request import Request
from rest_framework_jwt.authentication import JSONWebTokenAuthentication

from main.common.utils.utils import get_client_ip

from .models import OpLog

logger = logging.getLogger('post_data')


# Use MiddlewareMixin when present (Django >= 1.10)
try:
    from django.utils.deprecation import MiddlewareMixin
except ImportError:
    MiddlewareMixin = object


threadlocal = threading.local()


def is_authenticated(user):
    """Return whether or not a User is authenticated.
    Function provides compatibility following deprecation of method call to
    `is_authenticated()` in Django 2.0.
    This is *only* required to support Django < v1.10 (i.e. v1.9 and earlier),
    as `is_authenticated` was introduced as a property in v1.10.s
    """
    if not hasattr(user, 'is_authenticated'):
        return False
    if callable(user.is_authenticated):
        # Will be callable if django.version < 2.0, but is only necessary in
        # v1.9 and earlier due to change introduced in v1.10 making
        # `is_authenticated` a property instead of a callable.
        return user.is_authenticated()
    else:
        return user.is_authenticated


class AuditlogMiddleware(MiddlewareMixin):
    """
    Middleware to couple the request's user to log items. This is accomplished by currying the signal receiver with the
    user from the request (or None if the user is not authenticated).
    """

    def get_user_jwt(self, request):
        """
        Replacement for django session auth get_user & auth.get_user
        JSON Web Token authentication. Inspects the token for the user_id,
        attempts to get that user from the DB & assigns the user on the
        request object. Otherwise it defaults to AnonymousUser.

        This will work with existing decorators like LoginRequired  ;)

        Returns: instance of user object or AnonymousUser object
        """
        user = request._cached_user
        try:
            user_jwt = JSONWebTokenAuthentication().authenticate(Request(request))
            if user_jwt is not None:
                # store the first part from the tuple (user, obj)
                user = user_jwt[0] or user
        except Exception:
            pass
        return user or AnonymousUser()

    def process_request(self, request):
        """
        Gets the current user from the request and prepares and connects a signal receiver with the user already
        attached to it.
        """
        # Initialize thread local storage
        threadlocal.auditlog = {
            'signal_duid': (self.__class__, time.time()),
            'remote_addr': get_client_ip(request),
        }

        # Connect signal for automatic logging
        if hasattr(request, 'user') and is_authenticated(request.user):
            user = request.user
        else:
            user = SimpleLazyObject(lambda: self.get_user_jwt(request))
        if user:
            set_actor = curry(self.set_actor, user=user, signal_duid=threadlocal.auditlog['signal_duid'])
            pre_save.connect(set_actor, sender=OpLog, dispatch_uid=threadlocal.auditlog['signal_duid'], weak=False)

        logger.info(f'url:{request.get_full_path()}, user: {user}, post_data: {request.body[0:10*1024]}')

    def process_response(self, request, response):
        """
        Disconnects the signal receiver to prevent it from staying active.
        """
        if hasattr(threadlocal, 'auditlog'):
            pre_save.disconnect(sender=OpLog, dispatch_uid=threadlocal.auditlog['signal_duid'])

        # logger.info(
        #     f'url:{request.get_full_path()}, \
        #     user: {getattr(request, "user", None)}, response: {getattr(response, "data", None)}')

        return response

    def process_exception(self, request, exception):
        """
        Disconnects the signal receiver to prevent it from staying active in case of an exception.
        """
        if hasattr(threadlocal, 'auditlog'):
            pre_save.disconnect(sender=OpLog, dispatch_uid=threadlocal.auditlog['signal_duid'])

        return None

    @staticmethod
    def set_actor(user, sender, instance, signal_duid, **kwargs):
        """
        Signal receiver with an extra, required 'user' kwarg. This method becomes a real (valid) signal receiver when
        it is curried with the actor.
        """
        if signal_duid != threadlocal.auditlog['signal_duid']:
            return
        try:
            app_label, model_name = settings.AUTH_USER_MODEL.split('.')
            auth_user_model = apps.get_model(app_label, model_name)
        except ValueError:
            auth_user_model = apps.get_model('auth', 'user')
        if sender == OpLog and isinstance(user, auth_user_model) and not instance.operator:
            instance.user_id = user.id
            instance.operator = user.username
